home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2007 December / PCWKCD1207B.iso / Blogowanie poza sfera / Flock 1.0 beta / flock-1.0RC3.en-US.win32.exe / flock / components / flockTwitterService.js < prev    next >
Text File  |  2007-10-18  |  46KB  |  1,363 lines

  1. // BEGIN FLOCK GPL
  2. // 
  3. // Copyright Flock Inc. 2005-2007
  4. // http://flock.com
  5. // 
  6. // This file may be used under the terms of of the
  7. // GNU General Public License Version 2 or later (the "GPL"),
  8. // http://www.gnu.org/licenses/gpl.html
  9. // 
  10. // Software distributed under the License is distributed on an "AS IS" basis,
  11. // WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12. // for the specific language governing rights and limitations under the
  13. // License.
  14. // 
  15. // END FLOCK GPL
  16.  
  17. const CC = Components.classes;
  18. const CI = Components.interfaces;
  19. const CR = Components.results;
  20. const CU = Components.utils;
  21.  
  22. CU.import("resource:///modules/FlockXPCOMUtils.jsm");
  23. FlockXPCOMUtils.debug = false;
  24.  
  25. CU.import("resource:///modules/FlockScheduler.jsm");
  26. CU.import("resource:///modules/FlockSvcUtils.jsm");
  27. CU.import("resource://gre/modules/JSON.jsm");
  28.  
  29. const MODULE_NAME = "Twitter";
  30. const CLASS_NAME = "Flock Twitter Service";
  31. const CLASS_SHORT_NAME = "twitter";
  32. const CLASS_TITLE = "Twitter";
  33. const CLASS_ID = Components.ID("{535BFF20-9154-11DB-B606-0800200C9A66}");
  34. const CONTRACT_ID = "@flock.com/people/twitter;1";
  35.  
  36. const FLOCK_RICH_DND_CONTRACTID = "@flock.com/rich-dnd-service;1";
  37.  
  38. const SERVICE_ENABLED_PREF = "flock.service.twitter.enabled";
  39.  
  40. const FAVICON = "chrome://flock/content/services/twitter/favicon.png";
  41.  
  42. // From nsIXMLHttpRequest.idl
  43. const XMLHTTPREQUEST_READYSTATE_COMPLETED = 4;
  44.  
  45. const HTTP_CODE_OK = 200;
  46. const HTTP_CODE_FOUND = 302;
  47.  
  48. // The delay between two refreshes when the sidebar is closed (in seconds)
  49. const REFRESH_INTERVAL = 1800; // seconds == 30 minutes
  50. // The delay between two refreshes when the sidebar is open (in seconds)
  51. const SHORT_INTERVAL = 300; // seconds == 5 minutes
  52.  
  53. const TWITTER_URL = "http://www.twitter.com/"
  54.  
  55. // This is a workaround for the 401 errors we're getting when attempting to
  56. // authenticate to Twitter's API.  If we get a 401, we'll silently retry up to
  57. // this number of times before giving up and showing the user an error.
  58. const TWITTER_MAX_AUTH_ATTEMPTS = 5;
  59.  
  60. // Twitter returns friends in pages of 100.
  61. const TWITTER_FRIENDS_PAGE_SIZE = 100;
  62.  
  63. // Twitter returns messages in pages of 20.
  64. const TWITTER_MESSAGES_PAGE_SIZE = 20;
  65.  
  66. // Twitter API documentation specifies the maximum length of a status message.
  67. const TWITTER_MAX_STATUS_LENGTH = 140;
  68.  
  69. const TWITTER_PROPERTIES = "chrome://flock/locale/services/twitter.properties";
  70.  
  71. var gApi = null;
  72.  
  73. /*************************************************************************
  74.  * Component: flockTwitterService
  75.  *************************************************************************/
  76. function flockTwitterService() {
  77.   this._init();
  78.  
  79.   FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getAccount");
  80.   FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getAccounts");
  81.   FlockSvcUtils.flockIWebService.addDefaultMethod(this, "logout");
  82.  
  83.   FlockSvcUtils.flockIManageableWebService
  84.                .addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  85.   FlockSvcUtils.flockIManageableWebService
  86.                .addDefaultMethod(this, "getAccountIDFromDocument");
  87.   FlockSvcUtils.flockIManageableWebService
  88.                .addDefaultMethod(this, "getCredentialsFromForm");
  89.   FlockSvcUtils.flockIManageableWebService
  90.                .addDefaultMethod(this, "ownsDocument");
  91.   FlockSvcUtils.flockIManageableWebService
  92.                .addDefaultMethod(this, "ownsLoginForm");
  93.  
  94.   FlockSvcUtils.flockIRichContentDropHandler
  95.                .addDefaultMethod(this, "_handleTextareaDrop");
  96. }
  97.  
  98.  
  99. /*************************************************************************
  100.  * flockTwitterService: XPCOM Component Creation
  101.  *************************************************************************/
  102.  
  103. flockTwitterService.prototype = new FlockXPCOMUtils.genericComponent(
  104.   CLASS_NAME,
  105.   CLASS_ID,
  106.   CONTRACT_ID,
  107.   flockTwitterService,
  108.   CI.nsIClassInfo.SINGLETON,
  109.   [
  110.     CI.nsIObserver,
  111.     CI.flockIWebService,
  112.     CI.flockIManageableWebService,
  113.     CI.flockIPollingService,
  114.     CI.flockISocialWebService,
  115.     CI.flockIRichContentDropHandler
  116.   ]
  117.   );
  118.  
  119. // FlockXPCOMUtils.genericModule() categories
  120. flockTwitterService.prototype._xpcom_categories = [
  121.   { category: "wsm-startup" },
  122.   { category: "flockWebService", entry: CLASS_SHORT_NAME },
  123.   { category: "flockRichContentHandler", entry: CLASS_SHORT_NAME }  
  124. ];
  125.  
  126.  
  127. /*************************************************************************
  128.  * flockTwitterService: Private Data and Functions
  129.  *************************************************************************/
  130.  
  131. // Member variables.
  132. flockTwitterService.prototype._init =
  133. function fts_init() {
  134.   FlockSvcUtils.getLogger(this);
  135.   this._logger.debug(".init()");
  136.  
  137.   // Determine whether this service has been disabled, and unregister if so.
  138.   var prefService = CC["@mozilla.org/preferences-service;1"]
  139.                    .getService(CI.nsIPrefBranch);
  140.   if (prefService.getPrefType(SERVICE_ENABLED_PREF) &&
  141.      !prefService.getBoolPref(SERVICE_ENABLED_PREF))
  142.   {
  143.     this._logger.debug(SERVICE_ENABLED_PREF + " is FALSE! Not initializing.");
  144.     this.deleteCategories();
  145.     return;
  146.   }
  147.  
  148.   var profiler = CC["@flock.com/profiler;1"].getService(CI.flockIProfiler);
  149.   var evtID = profiler.profileEventStart("twitter-init");
  150.  
  151.   var obs = CC["@mozilla.org/observer-service;1"]
  152.             .getService(CI.nsIObserverService);
  153.   obs.addObserver(this, "xpcom-shutdown", false);
  154.  
  155.   this._accountClassCtor = flockTwitterAccount;
  156.  
  157.   FlockSvcUtils.getCoop(this);
  158.  
  159.   this._baseUrn = "urn:twitter";
  160.   this._serviceUrn = this._baseUrn + ":service";
  161.  
  162.   if (this._coop.Service.exists(this._serviceUrn)) {
  163.     this._c_svc = this._coop.get(this._serviceUrn);
  164.   } else {
  165.     this._c_svc = new this._coop.Service(this._serviceUrn, {
  166.       name: CLASS_SHORT_NAME,
  167.       desc: CLASS_TITLE
  168.     });
  169.   }
  170.   this._c_svc.serviceId = CONTRACT_ID;
  171.   this._c_svc.logoutOption = true;
  172.  
  173.   // Note that using FlockSvcUtils.getWD here adds the "_WebDetective"
  174.   // property to the service.
  175.   this._c_svc.domains = FlockSvcUtils.getWD(this)
  176.                         .getString("twitter", "domains", "twitter.com");
  177.   this._c_svc.loginURL = FlockSvcUtils.getWD(this)
  178.                          .getString("twitter",
  179.                                     "userlogin",
  180.                                     "https://twitter.com/login/");
  181.  
  182.   // Initialize API
  183.   gApi = new flockTwitterAPI();
  184.  
  185.   // Update auth states
  186.   try {
  187.     if (this._WebDetective.detectCookies("twitter", "loggedout", null)) {
  188.       this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
  189.     }
  190.   } catch (ex) {
  191.     this._logger.error("ERROR updating auth states for Twitter: " + ex);
  192.   }
  193.  
  194.   profiler.profileEventEnd(evtID, "");
  195. }
  196.  
  197.  
  198. flockTwitterService.prototype._lightPeopleIcon =
  199. function flockTwitterService__lightPeopleIcon() {
  200.   this._logger.debug("._lightPeopleIcon()");
  201.   var obs = CC["@mozilla.org/observer-service;1"]
  202.             .getService(CI.nsIObserverService);
  203.   obs.notifyObservers(null, "new-people-notification", null);
  204. }
  205.  
  206.  
  207. flockTwitterService.prototype._refreshAccount =
  208. function fts__refreshAccount(aUrn, aListener) {
  209.   var inst = this;
  210.   var refreshItem = this._coop.get(aUrn);
  211.   var lastUpdate = refreshItem.lastUpdateDate;
  212.   var numberOfAnswers = 0;
  213.  
  214.   // Ensures that both listeners have completed before sending our caller a
  215.   // response.  Also sets the next refresh time to be soon if the people
  216.   // sidebar is open.
  217.   function onIndividualSuccess() {
  218.     numberOfAnswers++;
  219.     if (numberOfAnswers >= 3) {
  220.       if (inst._acUtils.isPeopleSidebarOpen()) {
  221.         refreshItem.nextRefresh = new Date(Date.now() + SHORT_INTERVAL * 1000);
  222.       }
  223.       if (aListener) {
  224.         aListener.onResult();
  225.       }
  226.     }
  227.   }
  228.  
  229.   /**
  230.    * {
  231.    *   "status": {
  232.    *     "created_at": "Thu May 17 19:19:39 +0000 2007",
  233.    *     "text": "attending the full staff meeting",
  234.    *     "id": 67511202
  235.    *   },
  236.    *   "url": null,
  237.    *   "followers_count": 0,
  238.    *   "friends_count": 0,
  239.    *   "profile_background_color": "9ae4e8",
  240.    *   "name": "Matthew Willis",
  241.    *   "favourites_count": 0,
  242.    *   "profile_text_color": "000000",
  243.    *   "statuses_count": 1,
  244.    *   "profile_link_color": "0000ff",
  245.    *   "description": null,
  246.    *   "profile_sidebar_fill_color": "e0ff92",
  247.    *   "location": null,
  248.    *   "profile_image_url": "http:\/\/assets0.twitter.com\/images\/default_image.gif?1189634879",
  249.    *   "id": 6117302,
  250.    *   "utc_offset": -14400,
  251.    *   "profile_sidebar_border_color": "87bc44",
  252.    *   "screen_name": "lilmatt",
  253.    *   "protected": false
  254.    * }
  255.    */
  256.  
  257.   // This listener handles getting the account owner's information.
  258.   var userShowListener = {
  259.     onSuccess: function userShow_onSuccess(aResult) {
  260.       inst._logger.debug("Success for userShow");
  261.  
  262.       if (aResult.name) {
  263.         inst._logger.debug("userShow: name: " + aResult.name);
  264.         refreshItem.name = aResult.name;
  265.       }
  266.  
  267.       if (aResult.screen_name) {
  268.         inst._logger.debug("userShow: screen_name: " + aResult.screen_name);
  269.         refreshItem.screenName = aResult.screen_name;
  270.         refreshItem.URL = inst._WebDetective
  271.                               .getString(CLASS_SHORT_NAME, "userprofile", null)
  272.                               .replace("%accountid%", aResult.screen_name);
  273.       }
  274.  
  275.       if (aResult.profile_image_url) {
  276.         inst._logger.debug("userShow: avatar: " + aResult.profile_image_url);
  277.         // If avatar returned is the default image, set coop.Account.avatar
  278.         // to null and let the people sidebar code set the Flock common image.
  279.         if (inst._hasDefaultAvatar(aResult.profile_image_url)) {
  280.           inst._logger.debug("No avatar for account. Setting to null.");
  281.           refreshItem.avatar = null;
  282.         } else {
  283.           refreshItem.avatar = aResult.profile_image_url;
  284.         }
  285.       }
  286.  
  287.       if (aResult.status &&
  288.           aResult.status.text &&
  289.           aResult.status.text != refreshItem.statusMessage)
  290.       {
  291.         inst._logger.debug("userShow: status: " + aResult.status.text);
  292.         refreshItem.statusMessage = aResult.status.text;
  293.  
  294.         var dateString = aResult.status.created_at;
  295.         refreshItem.lastProfileUpdate = inst._parseTwitterDate(dateString);
  296.       }
  297.  
  298.       onIndividualSuccess();
  299.     },
  300.     onError: function userShow_onError(aError) {
  301.       inst._logger.error("Error on userShow");
  302.       aListener.onError(aError);
  303.     }
  304.   };
  305.  
  306.   // This listener handles getting the user's friends' information.
  307.   var friendsListener = {
  308.     onSuccess: function friends_onSuccess(aResult) {
  309.       inst._logger.debug("friendsListener onSuccess");
  310.  
  311.       // Now that we have all the results we need, update the RDF.
  312.       function myWorker(aShouldYield) {
  313.         // ADD or update existing people
  314.         for (var uid in aResult) {
  315.           inst._addPerson(aResult[uid], refreshItem);
  316.           if (aShouldYield()) {
  317.             yield;
  318.           }
  319.         }
  320.         // REMOVE locally people removed on the server
  321.         var localEnum = refreshItem.friendsList.children.enumerate();
  322.         while (localEnum.hasMoreElements()) {
  323.           var identity = localEnum.getNext();
  324.           if (!aResult[identity.accountId]) {
  325.             inst._logger.info("Friend " + identity.accountId
  326.                               + " has been deleted on the server");
  327.             refreshItem.friendsList.children.remove(identity);
  328.             identity.destroy();
  329.           }
  330.         }
  331.         onIndividualSuccess();
  332.       }
  333.       FlockScheduler.schedule(null, 0.05, 10, myWorker);
  334.     },
  335.     onError: function friends_onError(aError) {
  336.       inst._logger.error("friendsListener onError");
  337.       aListener.onError(aError);
  338.     }
  339.   };
  340.  
  341.   // This listener handles getting a count of the user's direct messages.
  342.   var messageCountListener = {
  343.     onSuccess: function messages_onSuccess(aResult) {
  344.       inst._logger.debug("messageCountListener onSuccess");
  345.       inst._logger.debug("Message count: " + aResult);
  346.       refreshItem.accountMessages = aResult;
  347.  
  348.       // Trigger the people icon highlight if the user has messages
  349.       if (aResult > 0) {
  350.         inst._lightPeopleIcon();
  351.       }
  352.  
  353.       onIndividualSuccess();
  354.     },
  355.     onError: function messages_onError(aError) {
  356.       inst._logger.error("messageCountListener onError");
  357.       aListener.onError(aError);
  358.     }
  359.   };
  360.  
  361.   gApi.userShow(refreshItem.accountId, userShowListener);
  362.   gApi.getFriendsStatus(null, friendsListener);
  363.   gApi.getTotalMessageCount(messageCountListener);
  364. }
  365.  
  366.  
  367. flockTwitterService.prototype._addPerson =
  368. function fts__addPerson(aPerson, aCoopAccount) {
  369.   // We include the current accountId in the identity urn to prevent friend
  370.   // collisions if multiple service accounts have the same friend.
  371.   var identityUrn = this._getIdentityUrn(aCoopAccount.accountId, aPerson.id);
  372.   var updating = this._coop.Identity.exists(identityUrn);
  373.   var identity;
  374.  
  375.   var lastUpdate = this._parseTwitterDate(aPerson.status.created_at);
  376.   this._logger.debug("Adding person: " + aPerson.id + " - " + aPerson.name);
  377.  
  378.   // XXX: Since Twitter only timestamps status changes, we need to add
  379.   //      property-wise profile comparison here.
  380.   var lastUpdateType = "status";
  381.  
  382.   var avatarUrl = null;
  383.   if (!this._hasDefaultAvatar(aPerson.profile_image_url)) {
  384.     avatarUrl = aPerson.profile_image_url;
  385.   }
  386.  
  387.   if (updating) {
  388.     // Update data of the coop.Identity object
  389.     identity = this._coop.get(identityUrn);
  390.     if (lastUpdate > identity.lastUpdate) {
  391.       identity.name = aPerson.name;
  392.       identity.avatar = avatarUrl;
  393.       identity.screenName = aPerson.screen_name;
  394.       identity.statusMessage = aPerson.status.text;
  395.       identity.lastUpdate = lastUpdate;
  396.       identity.lastUpdateType = lastUpdateType;
  397.     }
  398.   } else {
  399.     identity = new this._coop.Identity(
  400.       identityUrn,
  401.       {
  402.         name: aPerson.name,
  403.         serviceId: this.contractId,
  404.         accountId: aPerson.id,
  405.         avatar: avatarUrl,
  406.         screenName: aPerson.screen_name,
  407.         statusMessage: aPerson.status.text,
  408.         lastUpdate: lastUpdate,
  409.         lastUpdateType: lastUpdateType
  410.       }
  411.     );
  412.     aCoopAccount.friendsList.children.add(identity);
  413.   }
  414. }
  415.  
  416.  
  417. flockTwitterService.prototype._getIdentityUrn =
  418. function fts__getIdentityUrn(aAccountId, aUid) {
  419.   var prefix = "urn:flock:identity:" + CLASS_SHORT_NAME;
  420.   return prefix + aAccountId + ":" + aUid;
  421. }
  422.  
  423.  
  424. /**
  425.  * Helper function to parse Twitter's date string into seconds since epoch.
  426.  * @param  aDateString  A string formatted as: Wed Jan 31 00:16:35 +0000 2007
  427.  * @return  The number of seconds since the epoch.
  428.  */
  429. flockTwitterService.prototype._parseTwitterDate =
  430. function fts__parseTwitterDate(aDateString) {
  431.   this._logger.debug("_parseTwitterDate: in: " + aDateString);
  432.   if (!aDateString) {
  433.     return 0;
  434.   }
  435.   // Date.parse() returns milliseconds since epoch. Divide by 1000 for seconds.
  436.   return (Date.parse(aDateString) / 1000);
  437. }
  438.  
  439.  
  440. /**
  441.  * Helper function to determine if the user has customized their avatar based
  442.  * on the passed in URL.
  443.  * @param  aUrl  A string containing the contents of the Twitter user's
  444.  *               "profile_image_url" property.
  445.  * @return  true if the user is still using the default avatar, else false
  446.  */
  447. flockTwitterService.prototype._hasDefaultAvatar =
  448. function fts_hasDefaultAvatar(aUrl) {
  449.   this._logger.debug("_hasDefaultAvatar(" + aUrl + ")");
  450.   var defaultUrl = this._WebDetective.getString("twitter", "noAvatar", "");
  451.   return (aUrl.indexOf(defaultUrl) != -1);
  452. }
  453.  
  454.  
  455. /*************************************************************************
  456.  * flockTwitterService: flockIWebService Implementation
  457.  *************************************************************************/
  458.  
  459. // readonly attribute AString contractId;
  460. // ALMOST duplicated by nsIClassInfo::contractID
  461. // with different case: contractId != contractID
  462. flockTwitterService.prototype.contractId = CONTRACT_ID;
  463.  
  464. // readonly attribute AString icon;
  465. flockTwitterService.prototype.icon = FAVICON;
  466.  
  467. // readonly attribute boolean needPassword;
  468. flockTwitterService.prototype.needPassword = true;
  469.  
  470. // readonly attribute AString shortName;
  471. flockTwitterService.prototype.shortName = CLASS_SHORT_NAME;
  472.  
  473. // readonly attribute long status;
  474. flockTwitterService.prototype.status = CI.flockIWebService.STATUS_UNKNOWN;
  475.  
  476. // readonly attribute AString title;
  477. flockTwitterService.prototype.title = CLASS_TITLE;
  478.  
  479. // readonly attribute AString url;
  480. flockTwitterService.prototype.url = TWITTER_URL;
  481.  
  482. // readonly attribute AString urn;
  483. flockTwitterService.prototype.urn = "urn:" + CLASS_SHORT_NAME + ":service";
  484.  
  485. /**
  486.  * @see flockIWebService#addAccountById
  487.  */
  488. flockTwitterService.prototype.addAccountById =
  489. function fts_addAccountById(aAccountId, aIsTransient, aListener) {
  490.   this._logger.debug(".addAccountById('"
  491.                      + aAccountId + "', " + aIsTransient + ", aListener)");
  492.  
  493.   if (!aAccountId) {
  494.     aListener.onError();
  495.     return;
  496.   }
  497.  
  498.   var pw = this._acUtils.getPassword(this.urn + ":" + aAccountId);
  499.   var name = (pw) ? pw.user : aAccountId;
  500.  
  501.   var accountUrn = "urn:flock:" + this.shortName + ":account:" + aAccountId;
  502.   var account = new this._coop.Account(
  503.     accountUrn, {
  504.       name: name,
  505.       serviceId: this.contractId,
  506.       service: this._c_svc,
  507.       accountId: aAccountId,
  508.       isPollable: false,
  509.       isTransient: aIsTransient,
  510.       URL: this._WebDetective.getString(CLASS_SHORT_NAME, "userprofile", null)
  511.                .replace("%accountid%", aAccountId),
  512.       refreshInterval: REFRESH_INTERVAL,
  513.       favicon: FAVICON
  514.     });
  515.   this._coop.accounts_root.children.add(account);
  516.  
  517.   var friendsListUrn = accountUrn + ":friends";
  518.   var friendsList = new this._coop.FriendsList(
  519.     friendsListUrn,
  520.     {
  521.       account: account
  522.     });
  523.   account.friendsList = friendsList;
  524.  
  525.   // Instantiate account component
  526.   var acct = this.getAccount(account.id());
  527.   if (aListener) {
  528.     aListener.onSuccess(acct, "addAccount");
  529.   }
  530.   return acct;
  531. }
  532.  
  533. /**
  534.  * @see flockIWebService#removeAccount
  535.  */
  536. flockTwitterService.prototype.removeAccount =
  537. function fts_removeAccount(aUrn) {
  538.   this._logger.debug(".removeAccount(" + aUrn + ")");
  539.   this._acUtils.removeAccount(aUrn);
  540. }
  541.  
  542.  
  543. /*************************************************************************
  544.  * flockTwitterService: flockIManageableWebService Implementation
  545.  *************************************************************************/
  546.  
  547. // DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);
  548. // DEFAULT: AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
  549. // DEFAULT: nsIPassword getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);
  550. // DEFAULT: boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
  551. // DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);
  552.  
  553. /**
  554.  * void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument);
  555.  *
  556.  * @see flockIManageableWebService#updateAccountStatusFromDocument
  557.  */
  558. flockTwitterService.prototype.updateAccountStatusFromDocument =
  559. function fts_updateAccountStatusFromDocument(aDocument) {
  560.   this._logger.debug(".updateAccountStatusFromDocument(aDocument)");
  561.   if (this._WebDetective.detect(this.shortName, "loggedout", aDocument, null) ||
  562.       this._WebDetective.detectCookies(this.shortName, "loggedout", null))
  563.   {
  564.     this._logger.debug("We're logged out!");
  565.     this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
  566.   } else if (this._WebDetective.detect(this.shortName,
  567.                                        "loggedin",
  568.                                        aDocument,
  569.                                        null))
  570.   {
  571.     this._logger.debug("We're NOT logged out!  Let's try to get some creds!");
  572.     var results = FlockSvcUtils.newResults();
  573.     if (this._WebDetective.detect(this.shortName,
  574.                                   "accountinfo",
  575.                                   aDocument,
  576.                                   results))
  577.     {
  578.       var acctId = results.getPropertyAsAString("accountid");
  579.       var acctUrn = this._acUtils.getAccountURNById(this.urn, acctId);
  580.       var acct = this._coop.get(acctUrn);
  581.       if (!acct.isAuthenticated) {
  582.         acct.isAuthenticated = true;
  583.       }
  584.     }
  585.   } else {
  586.     this._logger.debug("We don't match 'loggedout' or 'logged in'");
  587.   }
  588. }
  589.  
  590. /*************************************************************************
  591.  * flockTwitterService: flockISocialWebService implementation
  592.  *************************************************************************/
  593.  
  594. flockTwitterService.prototype.maxStatusLength = TWITTER_MAX_STATUS_LENGTH;
  595.  
  596.  
  597. /*************************************************************************
  598.  * flockTwitterService: flockIPollingService Implementation
  599.  *************************************************************************/
  600. /**
  601.  * @see flockIPollingService#refresh
  602.  */
  603. flockTwitterService.prototype.refresh =
  604. function fts_refresh(aUrn, aListener) {
  605.   this._logger.debug(".refresh(" + aUrn + ")");
  606.   var refreshItem = this._coop.get(aUrn);
  607.  
  608.   if (refreshItem instanceof this._coop.Account) {
  609.     this._logger.debug("refreshing an Account");
  610.     if (refreshItem.isAuthenticated) {
  611.       this._refreshAccount(aUrn, aListener);
  612.     } else {
  613.       // If the user is not logged in, return a success without
  614.       // refreshing anything
  615.       aListener.onResult();
  616.     }
  617.   } else {
  618.     this._logger.error("can't refresh " + aUrn + " (unsupported type)");
  619.     aListener.onError(null);
  620.   }
  621. }
  622.  
  623.  
  624. /**************************************************************************
  625.  * flockTwitterService: nsIObserver Implementation
  626.  **************************************************************************/
  627.  
  628. flockTwitterService.prototype.observe =
  629. function fts_observe(aSubject, aTopic, aState) {
  630.   switch (aTopic) {
  631.     case "xpcom-shutdown":
  632.       var obs = CC["@mozilla.org/observer-service;1"]
  633.                 .getService(CI.nsIObserverService);
  634.       obs.removeObserver(this, "xpcom-shutdown");
  635.       break;
  636.   }
  637. }
  638.  
  639.  
  640. /**************************************************************************
  641.  * flockTwitterService: flockIRichContentDropHandler Implementation
  642.  **************************************************************************/
  643.  
  644. flockTwitterService.prototype.handleDrop =
  645. function fts_handleDrop(aFlavours, aTextarea) {
  646.   var inst = this;
  647.   var dropCallback = function facebook_dropCallback(aFlav) {
  648.     // Get URL from dropped text
  649.     var dataObj = {}, len = {};
  650.     aFlavours.getTransferData(aFlav, dataObj, len);
  651.     var text = dataObj.value.QueryInterface(CI.nsISupportsString).data;
  652.     var textParts = text.split(": ");
  653.     var url = (textParts.length == 2) ? textParts[1] : text;
  654.  
  655.     // Find position
  656.     var caretPos = aTextarea.selectionEnd;
  657.     var currentValue = aTextarea.value;
  658.  
  659.     // Add a trailing space so that we don't mangle the url
  660.     var nextChar = currentValue.charAt(caretPos);
  661.     var trailingSpace = ((nextChar == "") ||
  662.                          (nextChar == " ") ||
  663.                          (nextChar == "\n"))
  664.                       ? ""
  665.                       : " ";
  666.  
  667.     // Put it all together to drop the text into the selection. Note: no
  668.     // breadcrumb due to twitter length constraint.
  669.     aTextarea.value = currentValue.substring(0, caretPos)
  670.                     + url
  671.                     + trailingSpace
  672.                     + currentValue.substring(caretPos);
  673.   };
  674.  
  675.   return this._handleTextareaDrop(CLASS_SHORT_NAME, this._c_svc.domains,
  676.                                   aTextarea, dropCallback);
  677. }
  678.  
  679.  
  680. /*************************************************************************
  681.  * Component: flockTwitterAPI
  682.  *************************************************************************/
  683. function flockTwitterAPI() {
  684.   FlockSvcUtils.getLogger(this);
  685.   this._logger.init("twitterAPI");
  686.  
  687.   FlockSvcUtils.getACUtils(this);
  688.   FlockSvcUtils.getCoop(this);
  689.  
  690.   this._logger.debug("constructor");
  691. }
  692.  
  693.  
  694. /*************************************************************************
  695.  * flockTwitterAPI: XPCOM Component Creation
  696.  *************************************************************************/
  697.  
  698. // Use genericComponent() for the goodness it provides (QI, nsIClassInfo, etc),
  699. // but do NOT add this component to the list of constructors passed to
  700. // FlockXPCOMUtils.genericModule().
  701. flockTwitterAPI.prototype = new FlockXPCOMUtils.genericComponent(
  702.   CLASS_NAME + " API",
  703.   "",
  704.   "",
  705.   flockTwitterAPI,
  706.   0,
  707.   []
  708.   );
  709.  
  710.  
  711. /*************************************************************************
  712.  * flockTwitterAPI: Private data and functions
  713.  *************************************************************************/
  714. /**
  715.  * Actually do the API call.
  716.  * @param  aListener
  717.  * @param  aMethod
  718.  * @param  aParams
  719.  * @param  aRequestType  "GET" or "POST"
  720.  * @param  aPostVars  Array of JS objects to include in the POST body
  721.  * @param  aCount  A variable to increment when a 401 error occurs before
  722.  *                 trying again.  This is handled internally by this
  723.  *                 function.  External callers should set this to null.
  724.  */
  725. flockTwitterAPI.prototype._call =
  726. function api__call(aListener, aMethod, aParams, aRequestType, aPostVars,
  727.                    aCount)
  728. {
  729.   var requestType = aRequestType.toUpperCase();
  730.   var responseFormat = ".json";
  731.   var url = "https://twitter.com/" + aMethod + responseFormat;
  732.  
  733.   var idx = 0;
  734.   for (var p in aParams) {
  735.     // Only use "?" for the first param.  Use "&" after.
  736.     url += (idx == 0) ? "?" : "&";
  737.     url += p + "=" + aParams[p];
  738.     idx++;
  739.   }
  740.  
  741.   this._logger.debug("._call() url: " + url);
  742.  
  743.   var req = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
  744.             .createInstance(CI.nsIXMLHttpRequest)
  745.             .QueryInterface(CI.nsIJSXMLHttpRequest);
  746.  
  747.   // Don't pop nsIAuthPrompts if auth fails
  748.   req.backgroundRequest = true;
  749.  
  750.   var coopAccountUrn = this._acUtils
  751.                            .getFirstAuthenticatedAccountForService(CONTRACT_ID);
  752.   var coopAccount = this._coop.get(coopAccountUrn);
  753.   var passwordUrn = "urn:twitter:service:" + coopAccount.accountId;
  754.   var creds = this._acUtils.getPassword(passwordUrn);
  755.  
  756.   req.open(requestType, url, true, creds.user, creds.password);
  757.  
  758.   if (requestType == "POST") {
  759.     req.setRequestHeader("Content-Type",
  760.                          "application/x-www-form-urlencoded; charset=UTF-8");
  761.   }
  762.  
  763.   var inst = this;
  764.   req.onreadystatechange = function ORSC(aEvent) {
  765.     inst._logger.debug("._call() ReadyState: " + req.readyState);
  766.  
  767.     if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
  768.       try {
  769.         inst._logger.debug("._call() Status: " + req.status);
  770.  
  771.         if (req.status/100 == 2) {
  772.           inst._logger.debug("._call() response:\n" + req.responseText);
  773.           var s = new CU.Sandbox("about:blank");
  774.           var result = CU.evalInSandbox("(" + req.responseText + ")", s);
  775.  
  776.           if (result) {
  777.             aListener.onSuccess(result);
  778.           } else {
  779.             inst._logger.debug("._call() error: no result");
  780.             aListener.onError();
  781.           }
  782.  
  783.         } else if (req.status == 401) {
  784.           // XXX: Workaround for 401 errors we're seeing with Twitter.
  785.           // Loop call if necessary up to TWITTER_MAX_AUTH_ATTEMPTS times.
  786.  
  787.           // Increment counter
  788.           var count;
  789.           if (aCount == null) {
  790.             count = 1;
  791.           } else {
  792.             count = aCount + 1;
  793.           }
  794.  
  795.           if (count < TWITTER_MAX_AUTH_ATTEMPTS) {
  796.             // Try, try again
  797.             inst._logger.debug("._call() Got a 401. Will try "
  798.                                + (TWITTER_MAX_AUTH_ATTEMPTS - count)
  799.                                + " more time(s).");
  800.             inst._call(aListener, aMethod, aParams, aRequestType, aPostVars,
  801.                        count);
  802.           } else {
  803.             // Ok. Give up.
  804.             inst._logger.debug("._call() HTTP error");
  805.             aListener.onError(/*req.status*/);
  806.           }
  807.  
  808.         } else {
  809.           // HTTP errors
  810.           inst._logger.debug("._call() HTTP error");
  811.           aListener.onError(/*req.status*/);
  812.         }
  813.       } catch (ex) {
  814.         // XMLHTTPERROR (connection lost)
  815.         inst._logger.debug("._call() exception: " + ex);
  816.         aListener.onError(/*inst.getHTTPError("9001")*/);
  817.       }
  818.     }
  819.   }
  820.  
  821.   var postBody = "";
  822.   if (aPostVars) {
  823.     for (var v in aPostVars) {
  824.       if (postBody.length) {
  825.         postBody += "&";
  826.       }
  827.       postBody += v + "=" + encodeURIComponent(aPostVars[v]);
  828.     }
  829.   }
  830.  
  831.   if ((requestType == "POST") && postBody && postBody.length) {
  832.     this._logger.debug("._call() postBody: " + postBody);
  833.     req.send(postBody);
  834.   } else {
  835.     req.send(null);
  836.   }
  837. }
  838.  
  839.  
  840. /**
  841.  * Get info (including status) about a user.
  842.  * @param  aUid  Twitter uid of the user to query.
  843.  * @param  aListener
  844.  */
  845. flockTwitterAPI.prototype.userShow =
  846. function api_userShow(aUid, aListener) {
  847.   this._logger.debug(".userShow(" + aUid + ", aListener)");
  848.  
  849.   var url = "users/show/" + aUid;
  850.   gApi._call(aListener, url, null, "GET", null, null);
  851. }
  852.  
  853.  
  854. /**
  855.  * direct_messages
  856.  * Returns a list of the 20 most recent direct messages sent to the
  857.  * authenticating user.  The XML and JSON versions include detailed
  858.  * information about the sending and recipient users.
  859.  *
  860.  * URL: http://twitter.com/direct_messages.format
  861.  * Formats: xml, json, rss, atom
  862.  * Parameters:
  863.  *   since     Optional.  Narrows the resulting list of direct messages to just
  864.  *             those sent after the specified HTTP-formatted date.  The same
  865.  *             behavior is available by setting the If-Modified-Since parameter
  866.  *             in your HTTP request.
  867.  *             Ex: http://twitter.com/direct_messages.atom?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
  868.  *   since_id  Optional.  Returns only direct messages with an ID greater than
  869.  *             (that is, more recent than) the specified ID.
  870.  *             Ex: http://twitter.com/direct_messages.xml?since_id=12345
  871.  *   page      Optional.  Retrieves the 20 next most recent direct messages.
  872.  *             Ex: http://twitter.com/direct_messages.xml?page=3
  873.  */
  874. flockTwitterAPI.prototype.directMessages =
  875. function api_directMessages(aSince, aSinceId, aPage, aListener) {
  876.   this._logger.debug(".directMessages(" + aSince + ", "
  877.                                         + aSinceId + ", "
  878.                                         + aPage + ", "
  879.                                         + "aListener)");
  880.   var params = {};
  881.  
  882.   if (aSince) {
  883.     params.since = aSince;
  884.   }
  885.  
  886.   if (aSinceId) {
  887.     params.since_id = aSinceId;
  888.   }
  889.  
  890.   if (aPage) {
  891.     params.page = aPage;
  892.   }
  893.  
  894.   var url = "direct_messages";
  895.   gApi._call(aListener, url, params, "GET", null, null);
  896. }
  897.  
  898.  
  899. /**
  900.  * Get a count of all messages for the authenticated user
  901.  * @param  aListener
  902.  */
  903. flockTwitterAPI.prototype.getTotalMessageCount =
  904. function api_getTotalMessageCount(aListener) {
  905.   this._logger.debug(".getTotalMessageCount(aListener)");
  906.  
  907.   // We start off by asking for page 1 of messages.
  908.   var page = 1;
  909.  
  910.   // Message counter
  911.   var count = 0;
  912.  
  913.   var inst = this;
  914.   var getMessagesListener = {
  915.     onSuccess: function L_onSuccess(aResult) {
  916.       // If a full page is returned then go grab another
  917.       inst._logger.debug("Twitter messages returned: " + aResult.length);
  918.       count += aResult.length;
  919.       if (aResult.length == TWITTER_MESSAGES_PAGE_SIZE) {
  920.         inst._logger.debug("Fetching more messages");
  921.         page++;
  922.         inst.directMessages(null, null, page, getMessagesListener);
  923.       } else {
  924.         inst._logger.debug("Fetching messages complete");
  925.         aListener.onSuccess(count);
  926.       }
  927.     },
  928.     onError: function L_onError(aError) {
  929.       inst._logger.debug(".getMessagesListener() error: " + aError);
  930.     }
  931.   }
  932.  
  933.   this.directMessages(null, null, page, getMessagesListener);
  934. }
  935.  
  936.  
  937. /**
  938.  * Get a user's friends' updates.
  939.  * @param  aUid  Twitter uid of the user whose friends to view or null to
  940.  *               view the currently authenticated user's friends.
  941.  * @param  aListener
  942.  */
  943. flockTwitterAPI.prototype.getFriendsStatus =
  944. function api_getFriendsStatus(aUid, aListener) {
  945.   this._logger.debug(".getFriendsStatus(" + aUid + ", aListener)");
  946.  
  947.   var peopleHash = {};
  948.   var inst = this;
  949.  
  950.   var params = {};
  951.   // We start off by asking for page 1 of friends.
  952.   params.page = 1;
  953.  
  954.   var getFriendsListener = {
  955.     onSuccess: function L_onSuccess(aResult) {
  956.       for (var i in aResult) {
  957.         var id = aResult[i].id;
  958.         peopleHash[id] = aResult[i];
  959.  
  960.         // If there is no status item then stub one in
  961.         if (!peopleHash[id].status) {
  962.           peopleHash[id].status = {
  963.             created_at: 0,
  964.             text: "",
  965.             id: null
  966.           }
  967.         }
  968.  
  969.         inst._logger.debug("Got Twitter person: " + peopleHash[id].name);
  970.       }
  971.  
  972.       // If a full page is returned then go grab another
  973.       inst._logger.debug("Twitter friends returned: " + aResult.length);
  974.       if (aResult.length == TWITTER_FRIENDS_PAGE_SIZE) {
  975.         params.page++;
  976.         inst._logger.debug("Fetching more friends, now getting page "
  977.                            + params.page);
  978.         gApi._call(getFriendsListener, url, params, "GET", null, null);
  979.       } else {
  980.         inst._logger.debug("Fetching friends complete");
  981.         aListener.onSuccess(peopleHash);
  982.       }
  983.     },
  984.     onError: function L_onError(aError) {
  985.       inst._logger.debug(".getFriendsStatus() error: " + aError);
  986.     }
  987.   }
  988.  
  989.   var url = "statuses/friends";
  990.   if (aUid) {
  991.     url += "/" + aUid;
  992.   }
  993.   gApi._call(getFriendsListener, url, null, "GET", null, null);
  994. }
  995.  
  996.  
  997. /**
  998.  * Set the user's status
  999.  * @param  aStatusMessage  a string containing the message to set
  1000.  * @param  aListener
  1001.  *
  1002.  * Notes from Twitter API documentation:
  1003.  * -----------------------------------------------------------------------
  1004.  * Request must be a POST.
  1005.  * URL: http://twitter.com/statuses/update.format
  1006.  * Formats: xml, json.
  1007.  *   Returns the posted status in requested format when successful.
  1008.  * Parameters:
  1009.  *   status  Required  The text of your status update.
  1010.  *                     Be sure to URL encode as necessary.
  1011.  *                     Must not be more than 160 characters and should not
  1012.  *                     be more than 140 characters to ensure optimal display
  1013.  */
  1014. flockTwitterAPI.prototype.setStatus =
  1015. function api_setStatus(aStatusMessage, aListener) {
  1016.   this._logger.debug(".setStatus(" + aStatusMessage + ", aListener)");
  1017.  
  1018.   // substring() starts at 0 while TWITTER_MAX_STATUS_LENGTH counts from 1.
  1019.   var message = aStatusMessage.substring(0, (TWITTER_MAX_STATUS_LENGTH - 1));
  1020.  
  1021.   var postVars = {
  1022.     "source": "flock", // This value specified by Alex Payne at Twitter.
  1023.     "status": message
  1024.   };
  1025.   var url = "statuses/update";
  1026.   gApi._call(aListener, url, null, "POST", postVars, null);
  1027. }
  1028.  
  1029.  
  1030. /*************************************************************************
  1031.  * Component: flockTwitterAccount
  1032.  *************************************************************************/
  1033.  
  1034. function flockTwitterAccount() {
  1035.   FlockSvcUtils.getLogger(this);
  1036.   this._logger.init("twitterAccount");
  1037.  
  1038.   FlockSvcUtils.getACUtils(this);
  1039.   FlockSvcUtils.getCoop(this);
  1040.   // XXX: I should be able to use FlockSvcUtils.getWD() here, but it can
  1041.   //      only be called by the service.
  1042.   this._WebDetective = CC["@flock.com/web-detective;1"]
  1043.                        .getService(CI.flockIWebDetective);
  1044.  
  1045.   FlockSvcUtils.flockIWebServiceAccount.addDefaultMethod(this, "deactivate");
  1046.   FlockSvcUtils.flockIWebServiceAccount.addDefaultMethod(this, "login");
  1047.   FlockSvcUtils.flockIWebServiceAccount.addDefaultMethod(this, "logout");
  1048.   FlockSvcUtils.flockIWebServiceAccount.addDefaultMethod(this, "remove");
  1049.  
  1050.   FlockSvcUtils.flockISocialWebServiceAccount.addDefaultMethod(this, "getFriendCount");
  1051.  
  1052.   var sbs = CC["@mozilla.org/intl/stringbundle;1"]
  1053.             .getService(CI.nsIStringBundleService);
  1054.   this._bundle = sbs.createBundle(TWITTER_PROPERTIES);
  1055. }
  1056.  
  1057.  
  1058. /*************************************************************************
  1059.  * flockTwitterAccount: XPCOM Component Creation
  1060.  *************************************************************************/
  1061.  
  1062. // Use genericComponent() for the goodness it provides (QI, nsIClassInfo, etc),
  1063. // but do NOT add this component to the list of constructors passed to
  1064. // FlockXPCOMUtils.genericModule().
  1065. flockTwitterAccount.prototype = new FlockXPCOMUtils.genericComponent(
  1066.   CLASS_NAME + " Account",
  1067.   "",
  1068.   "",
  1069.   flockTwitterAccount,
  1070.   0,
  1071.   [
  1072.     CI.flockIWebServiceAccount,
  1073.     CI.flockISocialWebServiceAccount
  1074.   ]
  1075.   );
  1076.  
  1077.  
  1078. /*************************************************************************
  1079.  * flockTwitterAccount: flockIWebServiceAccount Implementation
  1080.  *************************************************************************/
  1081.  
  1082. // readonly attribute AString urn;
  1083. flockTwitterAccount.prototype.urn = "";
  1084.  
  1085. // void activate(in flockIListener aListener);
  1086. flockTwitterAccount.prototype.activate =
  1087. function fta_activate(aListener) {
  1088.   this._logger.debug(".activate()");
  1089.   var acctCoopObj = this._coop.get(this.urn);
  1090.   acctCoopObj.isAuthenticated = true;
  1091.   acctCoopObj.isPollable = true;
  1092.  
  1093.   if (aListener) {
  1094.     aListener.onSuccess(this, "activate");
  1095.   }
  1096. }
  1097.  
  1098. // DEFAULT: void deactivate(in flockIListener aListener);
  1099. // DEFAULT: void login(in flockIListener aListener);
  1100. // DEFAULT: void logout(in flockIListener aListener);
  1101.  
  1102. // void keep();
  1103. flockTwitterAccount.prototype.keep =
  1104. function fta_keep() {
  1105.   this._logger.debug(".keep()");
  1106.   this._coop.get(this.urn).isTransient = false;
  1107.   var urn = this.urn.replace("account:", "service:").replace("flock:", "");
  1108.   this._acUtils.makeTempPasswordPermanent(urn);
  1109. }
  1110.  
  1111. // DEFAULT: void remove();
  1112.  
  1113.  
  1114. /*************************************************************************
  1115.  * flockTwitterAccount: flockISocialWebServiceAccount Implementation
  1116.  *************************************************************************/
  1117. // readonly attribute boolean hasFriendActions;
  1118. flockTwitterAccount.prototype.hasFriendActions = true;
  1119.  
  1120. // readonly attribute boolean isPostLinkSupported;
  1121. flockTwitterAccount.prototype.isPostLinkSupported = true;
  1122.  
  1123. // readonly attribute boolean isMyMediaFavoritesSupported;
  1124. flockTwitterAccount.prototype.isMyMediaFavoritesSupported = false;
  1125.  
  1126. // readonly attribute boolean isStatusSupported;
  1127. flockTwitterAccount.prototype.isStatusSupported = true;
  1128.  
  1129. // readonly attribute boolean isStatusEditable;
  1130. flockTwitterAccount.prototype.isStatusEditable = true;
  1131.  
  1132.  
  1133. // AString formatStatusForDisplay(in AString aStatusMessage);
  1134. flockTwitterAccount.prototype.formatStatusForDisplay =
  1135. function fma_formatStatusForDisplay(aStatusMessage) {
  1136.   this._logger.debug(".formatStatusForDisplay(" + aStatusMessage + ")");
  1137.  
  1138.   var message = (aStatusMessage) ? aStatusMessage : "";
  1139.  
  1140.   // Responses from Twitter contain these HTML entities.  Order is important!
  1141.   message = message.replace(/&/g, "&")
  1142.                    .replace(/>/g, ">")
  1143.                    .replace(/</g, "<")
  1144.                    .replace(/"/g, '"');
  1145.   return message;
  1146. }
  1147.  
  1148.  
  1149. // AString getProfileURLForFriend(in AString aFriendUrn);
  1150. flockTwitterAccount.prototype.getProfileURLForFriend =
  1151. function fta_getProfileURLForFriend(aFriendUrn) {
  1152.   this._logger.debug(".getProfileURLForFriend('" + aFriendUrn + "')");
  1153.  
  1154.   var url = "";
  1155.   var coopFriend = this._coop.get(aFriendUrn);
  1156.   var screenName = coopFriend.screenName;
  1157.  
  1158.   if (screenName) {
  1159.     url = this._WebDetective.getString(CLASS_SHORT_NAME, "friendProfile", "")
  1160.                             .replace("%screenName%", screenName);
  1161.   }
  1162.  
  1163.   this._logger.debug(".getProfileURLForFriend()  url: " + url);
  1164.   return url;
  1165. }
  1166.  
  1167.  
  1168. // void setStatus(in AString aStatusMessage, in flockIListener aListener);
  1169. flockTwitterAccount.prototype.setStatus =
  1170. function fma_setStatus(aStatusMessage, aListener) {
  1171.   this._logger.debug(".setStatus(" + aStatusMessage + ")");
  1172.  
  1173.   var inst = this;
  1174.   var statusListener = {
  1175.     onSuccess: function L_onSuccess(aResult) {
  1176.       // If the API call succeeded, also set the coop.Account status.
  1177.       inst._coop.get(inst.urn).statusMessage = aStatusMessage;
  1178.       if (aListener) {
  1179.         aListener.onSuccess(aResult, null);
  1180.       }
  1181.     },
  1182.     onError: function L_onError(aError) {
  1183.       if (aListener) {
  1184.         aListener.onError(null, null, aError);
  1185.       }
  1186.     }
  1187.   }
  1188.   gApi.setStatus(aStatusMessage, statusListener);
  1189. }
  1190.  
  1191.  
  1192. // AString getEditableStatus();
  1193. flockTwitterAccount.prototype.getEditableStatus =
  1194. function fma_getEditableStatus() {
  1195.   this._logger.debug(".getEditableStatus()");
  1196.   var message = this._coop.get(this.urn).statusMessage;
  1197.   return this.formatStatusForDisplay(message);
  1198. }
  1199.  
  1200.  
  1201. // AString getMeNotifications();
  1202. flockTwitterAccount.prototype.getMeNotifications =
  1203. function fma_getMeNotifications() {
  1204.   this._logger.debug(".getMeNotifications()");
  1205.  
  1206.   var noties = [];
  1207.   var inst = this;
  1208.   function _addNotie(aType, aCount) {
  1209.     var stringName = "flock." + CLASS_SHORT_NAME + ".noties." + aType + "."
  1210.                    + ((parseInt(aCount) <= 0) ? "none" : "some");
  1211.  
  1212.     inst._logger.debug("aType: " + aType
  1213.                        + " aCount: " + aCount
  1214.                        + " name: " + stringName);
  1215.     noties.push({
  1216.       class: aType,
  1217.       tooltip: inst._bundle.GetStringFromName(stringName),
  1218.       count: aCount,
  1219.       URL: inst._WebDetective.getString(CLASS_SHORT_NAME, aType + "_URL", "")
  1220.     });
  1221.   }
  1222.   var coopAccount = this._coop.get(this.urn);
  1223.   _addNotie("meMessages", coopAccount.accountMessages);
  1224.  
  1225.   return JSON.toString(noties);
  1226. }
  1227.  
  1228. flockTwitterAccount.prototype.markAllMeNotificationsSeen =
  1229. function flockTwitterAccount_markAllMeNotificationsSeen(aType) {
  1230.   this._logger.debug(".markAllMeNotificationsSeen('" + aType + "')");
  1231.   var c_acct = this._coop.get(this.urn);
  1232.   switch (aType) {
  1233.     case "meMessages":
  1234.       c_acct.accountMessages = 0;
  1235.       break;
  1236.     default:
  1237.       break;
  1238.   }
  1239. }
  1240.  
  1241. // AString getFriendActions(in AString aFriendUrn);
  1242. flockTwitterAccount.prototype.getFriendActions =
  1243. function fma_getFriendActions(aFriendUrn) {
  1244.   this._logger.debug(".getFriendActions('" + aFriendUrn + "')");
  1245.  
  1246.   var actionNames = ["friendMessage",
  1247.                      "friendNudge",
  1248.                      "friendViewProfile"];
  1249.  
  1250.   var actions = [];
  1251.   var coopFriend = this._coop.get(aFriendUrn);
  1252.   if (coopFriend) {
  1253.     var coopAccount = this._coop.get(this.urn);
  1254.     for each (var action in actionNames) {
  1255.       actions.push({
  1256.         label: this._bundle.GetStringFromName("flock."
  1257.                                               + CLASS_SHORT_NAME
  1258.                                               + ".actions." + action),
  1259.         class: action,
  1260.         spec: this._WebDetective.getString(CLASS_SHORT_NAME, action, "")
  1261.                   .replace("%accountid%", coopAccount.accountId)
  1262.                   .replace("%friendid%", coopFriend.accountId)
  1263.       });
  1264.     }
  1265.   }
  1266.  
  1267.   return JSON.toString(actions);
  1268. }
  1269.  
  1270.  
  1271. // AString getSharingAction(in AString aFriendUrn,
  1272. //                          in nsITransferable aTransferable);
  1273. flockTwitterAccount.prototype.getSharingAction =
  1274. function fma_getSharingAction(aFriendUrn, aTransferable) {
  1275.   this._logger.debug(".getSharingAction('" + aFriendUrn + "')");
  1276.  
  1277.   var sharingAction = "";
  1278.   var coopFriend = this._coop.get(aFriendUrn);
  1279.   if (!coopFriend) {
  1280.     return sharingAction;
  1281.   }
  1282.  
  1283.   var flavours = ["text/x-flock-media",
  1284.                   "text/x-moz-url",
  1285.                   "text/unicode"];
  1286.  
  1287.   var message = CC[FLOCK_RICH_DND_CONTRACTID]
  1288.                 .getService(CI.flockIRichDNDService)
  1289.                 .getMessageFromTransferable(aTransferable,
  1290.                                             flavours.length,
  1291.                                             flavours);
  1292.   if (!message.body) {
  1293.     return sharingAction;
  1294.   }
  1295.  
  1296.   sharingAction = this._WebDetective
  1297.                       .getString(CLASS_SHORT_NAME,
  1298.                                  "shareAction_directMessage", "")
  1299.                       .replace("%friendid%", coopFriend.accountId)
  1300.                       .replace("%message%", encodeURIComponent(message.body));
  1301.  
  1302.   this._logger.debug(".getSharingAction(): " + sharingAction);
  1303.   return sharingAction;
  1304. }
  1305.  
  1306.  
  1307. flockTwitterAccount.prototype.getPostLinkAction =
  1308. function fta_getPostLinkAction(aTransferable) {
  1309.   var postLinkAction = "";
  1310.   var url = "";
  1311.  
  1312.   if (aTransferable) {
  1313.     // Something was dropped onto the "Post Link" button: get the URL from the
  1314.     // transferable
  1315.     var flavours = ["text/x-flock-media",
  1316.                     "text/x-moz-url",
  1317.                     "text/unicode"];
  1318.  
  1319.     var message = CC[FLOCK_RICH_DND_CONTRACTID]
  1320.                   .getService(CI.flockIRichDNDService)
  1321.                   .getMessageFromTransferable(aTransferable,
  1322.                                               flavours.length,
  1323.                                               flavours);
  1324.  
  1325.     url = message.body;
  1326.   } else {
  1327.     // The "Post Link" button was clicked: get the current tab's URL
  1328.     var win = CC["@mozilla.org/appshell/window-mediator;1"]
  1329.               .getService(CI.nsIWindowMediator)
  1330.               .getMostRecentWindow("navigator:browser");
  1331.     if (win) {
  1332.       url = win.gBrowser.currentURI.spec;
  1333.     }
  1334.   }
  1335.  
  1336.   if (url) {
  1337.     postLinkAction = this._WebDetective
  1338.                          .getString(CLASS_SHORT_NAME,
  1339.                                     "shareAction_publicMessage",
  1340.                                     "")
  1341.                          .replace("%message%", encodeURIComponent(url));
  1342.   }
  1343.  
  1344.   return postLinkAction;
  1345. }
  1346.  
  1347.  
  1348. /*************************************************************************
  1349.  * flockTwitterAccount Private Data and Functions
  1350.  *************************************************************************/
  1351.  
  1352.  
  1353. /*************************************************************************
  1354.  * XPCOM Support - Module Construction
  1355.  *************************************************************************/
  1356.  
  1357. // Create array of components.
  1358. var componentsArray = [flockTwitterService];
  1359.  
  1360. // Generate a module for XPCOM to find.
  1361. var NSGetModule = FlockXPCOMUtils.generateNSGetModule(MODULE_NAME,
  1362.                                                       componentsArray);
  1363.